
//  Copyright (C) 2012 Luca Piccioni
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
// 
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

namespace Derm.Render
{
	/// <summary>
	/// Class describing a partial uniform state of a <see cref="ShaderProgram"/>
	/// </summary>
	public abstract class ShaderUniformState : RenderState
	{
		#region Constructors

		/// <summary>
		/// 
		/// </summary>
		protected ShaderUniformState()
		{
			mUniforms = MapUniforms(GetType());
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="uniformContainer"></param>
		protected ShaderUniformState(object uniformContainer)
		{
			mUniforms = MapUniforms(uniformContainer.GetType());
		}

		#endregion

		#region Reflected Uniform Bindings

		/// <summary>
		/// The number of uniforms specified by this state.
		/// </summary>
		public uint UniformsCount { get { return ((uint)mUniforms.Count); } }

		/// <summary>
		/// Create a map between an uniform value and a <see cref="UniformBinding"/> storing information about the uniform.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		private static Dictionary<string,UniformBinding> MapUniforms(Type type)
		{
			Dictionary<string,UniformBinding> uniformMap = new Dictionary<string,UniformBinding>();

			if (type == null)
				throw new ArgumentNullException("type");

			// Uniforms specified as fields
			FieldInfo[] allFields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);

			foreach (FieldInfo fieldInfo in allFields) {
				Attribute[] attribs = Attribute.GetCustomAttributes(fieldInfo, typeof(RenderObjectUniformAttribute), true);

				if ((attribs != null) && (attribs.Length > 0)) {
					foreach (Attribute attrib in attribs) {
						RenderObjectUniformAttribute uniformAttribute = (RenderObjectUniformAttribute)attrib;

						if (uniformMap.ContainsKey(uniformAttribute.BindingName) == true)
							throw new InvalidOperationException(String.Format("{0} has an ambiguos uniform binding '{1}'", type, uniformAttribute.BindingName));

						uniformMap.Add(uniformAttribute.BindingName, new UniformBinding(uniformAttribute, fieldInfo));
					}
				}
			}

			// Uniforms specified as properties
			PropertyInfo[] allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

			foreach (PropertyInfo propertyInfo in allProperties) {
				Attribute[] attribs = Attribute.GetCustomAttributes(propertyInfo, typeof(RenderObjectUniformAttribute), true);

				if ((attribs != null) && (attribs.Length > 0)) {
					foreach (Attribute attrib in attribs) {
						RenderObjectUniformAttribute uniformAttribute = (RenderObjectUniformAttribute)attrib;

						if (uniformMap.ContainsKey(uniformAttribute.BindingName) == true)
							throw new InvalidOperationException(String.Format("{0} has an ambiguos uniform binding '{1}'", type, uniformAttribute.BindingName));

						//if (uniformAttribute.Cacheable == false)
							uniformMap.Add(uniformAttribute.BindingName, new UniformBinding(uniformAttribute, propertyInfo));
						//else
						//	uniformMap.Add(uniformAttribute.BindingName, new UniformBinding(uniformAttribute, propertyInfo));
					}
				}
			}

			// Uniforms specifies as methods
			MethodInfo[] allMethods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public);

			foreach (MethodInfo methodInfo in allMethods) {
				Attribute[] attribs = Attribute.GetCustomAttributes(methodInfo, typeof(RenderObjectUniformAttribute), true);

				if ((attribs != null) && (attribs.Length > 0)) {
					ParameterInfo[] infoParams = methodInfo.GetParameters();

					// Required checks
					if (methodInfo.ReturnType == typeof(void))
						throw new InvalidOperationException(String.Format("{0} uniform binding '{1}' returns void", methodInfo.Name, methodInfo.Name));
					if (infoParams.Length != 1)
						throw new InvalidOperationException(String.Format("{0} uniform binding '{1}' takes invalid argument count", methodInfo.Name, methodInfo.Name));
					if (infoParams[0].ParameterType != typeof(RenderContext))
						throw new InvalidOperationException(String.Format("{0} uniform binding '{1}' takes invalid argument type (RenderContext)", methodInfo.Name, methodInfo.Name));

					foreach (Attribute attrib in attribs) {
						RenderObjectUniformAttribute uniformAttribute = (RenderObjectUniformAttribute)attrib;

						if (uniformMap.ContainsKey(uniformAttribute.BindingName) == true)
							throw new InvalidOperationException(String.Format("{0} has an ambiguos uniform binding '{0}'", uniformAttribute.BindingName));
						//if (uniformAttribute.Cacheable == false)
							uniformMap.Add(uniformAttribute.BindingName, new UniformBinding(uniformAttribute, methodInfo));
						//else
						//	uniformMap.Add(uniformAttribute.BindingName, new UniformBinding(uniformAttribute, methodInfo));
					}
				}
			}

			return (uniformMap);
		}

		/// <summary>
		/// Determine whether this RenderResource defines an uniform binding.
		/// </summary>
		/// <param name="binding"></param>
		/// <returns></returns>
		protected bool HasUniformBinding(string binding)
		{
			if (String.IsNullOrEmpty(binding) == true)
				throw new ArgumentException("invalid value", "binding");

			return (mUniforms.ContainsKey(binding));
		}

		/// <summary>
		/// Determine the type of the uniform binding.
		/// </summary>
		/// <param name="binding"></param>
		/// <returns></returns>
		protected Type GetUniformType(string binding)
		{
			if (HasUniformBinding(binding) == false)
				throw new ArgumentException("no binding", "binding");

			return (mUniforms[binding].DeclaredBindingType);
		}

		/// <summary>
		/// Get the updated value of the uniform.
		/// </summary>
		/// <param name="ctx"></param>
		/// <param name="binding"></param>
		/// <returns></returns>
		protected object GetUniformValue(RenderContext ctx, string binding, object container)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (ctx.DebugEnabled == true) {
				if (HasUniformBinding(binding) == false)
					throw new ArgumentException("no binding", "binding");
			}

			return (mUniforms[binding].GetBindingValue(ctx, container));
		}

		/// <summary>
		/// Class used for inspect uniform values using reflection.
		/// </summary>
		private class UniformBinding : RenderObjectBase.ReflectedBinding
		{
			#region Constructors

			/// <summary>
			/// Construct an uniform binding based on field implementation.
			/// </summary>
			/// <param name="attribute">
			/// The <see cref="RenderObjectUniformAttribute"/> specified on <paramref name="info"/> declaration.
			/// </param>
			/// <param name="info">
			/// A <see cref="FieldInfo"/> pointing to the field returning an uniform value.
			/// </param>
			public UniformBinding(RenderObjectUniformAttribute attribute, FieldInfo info) : base(attribute, info)
			{

			}

			/// <summary>
			/// Construct an uniform binding based on property implementation.
			/// </summary>
			/// <param name="attribute">
			/// The <see cref="RenderObjectUniformAttribute"/> specified on <paramref name="info"/> declaration.
			/// </param>
			/// <param name="info">
			/// A <see cref="PropertyInfo"/> pointing to the property returning an uniform value.
			/// </param>
			public UniformBinding(RenderObjectUniformAttribute attribute, PropertyInfo info) : base(attribute, info)
			{

			}

			/// <summary>
			/// Construct an uniform binding based on property implementation.
			/// </summary>
			/// <param name="attribute">
			/// The <see cref="RenderObjectUniformAttribute"/> specified on <paramref name="info"/> declaration.
			/// </param>
			/// <param name="info">
			/// A <see cref="PropertyInfo"/> pointing to the property returning an uniform value.
			/// </param>
			public UniformBinding(RenderObjectUniformAttribute attribute, MethodInfo info) : base(attribute, info)
			{

			}

			#endregion
		}

		/// <summary>
		/// Map between uniform binding name and the member able to specify uniform value.
		/// </summary>
		private readonly Dictionary<string, UniformBinding> mUniforms;

		#endregion

		#region Uniform Block Support

		/// <summary>
		/// Get the shader uniform block semantic (used if the renderer support OpenGL uniform blocks).
		/// </summary>
		public virtual string BlockSemantic { get { return (null); } }

		#endregion

		#region Shader Uniform State Application

		/// <summary>
		/// Apply this state to a shader program.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used to share common state between shaders.
		/// </param>
		/// <param name="shaderProgram">
		/// A <see cref="ShaderProgram"/> that specify this uniform state.
		/// </param>
		private void ApplyState(RenderContext ctx, ShaderProgram shaderProgram, string uniformScope)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (shaderProgram == null)
				throw new ArgumentNullException("shaderProgram");

			foreach (KeyValuePair<string, UniformBinding> pair in mUniforms) {
				Type uniformType = GetUniformType(pair.Key);
				object uniformValue = GetUniformValue(ctx, pair.Key, this);

				if (uniformType.GetInterface("IEnumerable") != null) {
					IEnumerable enumerable = (IEnumerable)uniformValue;
					int arrayIndex = 0;

					foreach (object obj in enumerable) {
						string uniformSubScope;

						if (String.IsNullOrEmpty(uniformScope) == false)
							uniformSubScope = String.Format("{0}.{1}[{2}]", uniformScope, pair.Key, arrayIndex);
						else
							uniformSubScope = String.Format("{0}[{1}]", pair.Key, arrayIndex);

						if (obj == null)
							throw new InvalidOperationException("null uniform array item");
						if (obj.GetType().IsSubclassOf(typeof(ShaderUniformState)) == false)
							throw new InvalidOperationException("uniform array item not an uniform state");

						ShaderUniformState subUniformState = (ShaderUniformState) obj;

						subUniformState.ApplyState(ctx, shaderProgram, uniformSubScope);

						arrayIndex++;
					}
				} else if (typeof(ShaderUniformState).IsAssignableFrom(uniformType) == true) {
					ShaderUniformState uniformState = (ShaderUniformState) uniformValue;
					string uniformName = (String.IsNullOrEmpty(uniformScope) == false) ? String.Format("{0}.{1}", uniformScope, pair.Key) : pair.Key;

					// ShaderUniformState case
					uniformState.ApplyState(ctx, shaderProgram, uniformName);

				} else {
					string uniformName = (String.IsNullOrEmpty(uniformScope) == false) ? String.Format("{0}.{1}", uniformScope, pair.Key) : pair.Key;

					// Set uniform value
					if (shaderProgram.IsActiveUniform(uniformName) && (uniformValue != null))
						shaderProgram.SetUniform(ctx, uniformName, uniformValue);

				}
			}
		}

		#endregion

		#region RenderState Overrides

		/// <summary>
		/// The identifier of this RenderState.
		/// </summary>
		public override string Id { get { return (null); } }

		/// <summary>
		/// Flag indicating whether the state is context-bound.
		/// </summary>
		/// <remarks>
		/// It returns always false.
		/// </remarks>
		public override bool IsContextBound { get { return (false); } }

		/// <summary>
		/// Apply this depth test render state.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> which has defined the shader program <paramref name="shaderProgram"/>.
		/// </param>
		/// <param name="shaderProgram">
		/// The <see cref="ShaderProgram"/> which has the state set.
		/// </param>
		public override void ApplyState(RenderContext ctx, ShaderProgram shaderProgram)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (shaderProgram == null)
				throw new ArgumentNullException("shaderProgram");
			if (ctx.DebugEnabled) {
				if (ctx.IsCurrent() == false)
					throw new ArgumentException("not current", "ctx");
			}

			// Apply uniforms found using reflection
			ApplyState(ctx, shaderProgram, String.Empty);
		}

		/// <summary>
		/// Indicates whether the current object is equal to another object of the same type.
		/// </summary>
		/// <param name="other">
		/// A <see cref="RenderState"/> to compare to this RenderState.
		/// </param>
		/// <returns>
		/// It returns true if the current object is equal to <paramref name="other"/>.
		/// </returns>
		/// <remarks>
		/// <para>
		/// This method test only whether <paramref name="other"/> type equals to this type.
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentNullException">
		/// This exception is thrown if the parameter <paramref name="other"/> is null.
		/// </exception>
		public override bool Equals(IRenderState other)
		{
			if (base.Equals(other) == false)
				return (false);
			Debug.Assert(other is ShaderUniformState);

			ShaderUniformState otherState = (ShaderUniformState) other;
			RenderContext ctx = RenderContext.GetCurrentContext();

			if (ctx == null)
				throw new InvalidOperationException("no current context");

			return (false);
		}

		public void CreateState(RenderContext ctx, ShaderProgram shaderProgram)
		{
			foreach (string uniformBinding in shaderProgram.ActiveUniforms) {
				if (HasUniformBinding(uniformBinding)) {
					Type uniformType = GetUniformType(uniformBinding);

					if (uniformType.GetInterface("IRenderResource") != null) {
						object uniformObject = GetUniformValue(ctx, uniformBinding, this);
						IRenderResource renderResource = (IRenderResource)uniformObject;

						if (renderResource.Exists(ctx) == false)
							renderResource.Create(ctx);
					}
				}
			}
		}

		#endregion
	}

	/// <summary>
	/// 
	/// </summary>
	class ShaderUniformContainerState : ShaderUniformState
	{
		#region Constructors

		/// <summary>
		/// 
		/// </summary>
		/// <param name="uniformContainer"></param>
		public ShaderUniformContainerState(object uniformContainer) : base(uniformContainer)
		{
			
		}

		#endregion

		#region ShaderUniformState Overrides

		public override string Id
		{
			get { throw new NotImplementedException(); }
		}

		#endregion
	}
}
